package com.wutong.erb.filter;

import com.wutong.erb.entity.user.User;
import com.wutong.erb.util.DeviceUtil;
import com.wutong.erb.util.JwtUtil;
import com.wutong.erb.util.SecretUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

/**
 * 注册认证 JWT 过滤器配置
 * <pre>{@code
 * response.setHeader("Authorization", "Bearer " + JwtUtil.generateToken(
 *                 "AuthorizationToken",
 *                 Map.of(
 *                         "deviceIp", request.getRemoteAddr(),
 *                         "device", deviceHash,
 *                         "requestTime", System.currentTimeMillis()
 *                 ), 10, TimeUnit.MINUTES
 * ));
 * }</pre>
 * @author wutong
 */
@Slf4j
@RequiredArgsConstructor
public class RegisterJwtAuthenticationFilter extends OncePerRequestFilter {

    private final RedisTemplate<String, String> redisTemplate;

    public static final String[] REGISTER_WHITE_LIST = {
            "/api/auth/check-username"
    };

    /**
     * 处理注册需要的各种请求，包括请求验证码，刷新验证码，注册，发送邮箱验证码
     * @param request HTTP 请求
     * @param response HTTP 响应
     * @param filterChain 过滤器链
     * @throws ServletException 服务异常
     * @throws IOException IO 异常
     */
    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                    @NotNull HttpServletResponse response,
                                    @NotNull FilterChain filterChain)
            throws ServletException, IOException {
        log.debug("RegisterJwtAuthenticationFilter doFilterInternal");
        if (shouldNotFilter(request)) {
            log.debug("请求在白名单中，放行");
            filterChain.doFilter(request, response);
            return;
        }
        Cookie cookie = null;
        try {
            for (Cookie c : request.getCookies()) {
                log.debug("cookie name:{}", c.getName());
                if ("token".equalsIgnoreCase(c.getName())) {
                    log.debug("获取到cookie中的token");
                    cookie = c;
                    break;
                }
            }
            if (cookie == null) {
                log.debug("Cookie未找到，验证失败");
                response.sendError(401, "验证失败");
                return;
            }
        } catch (Exception e) {
            log.debug("Cookie不存在，验证失败");
            response.sendError(401, "验证失败");
            return;
        }
        String cookieToken = cookie.getValue();

        // 解析Token，验证Token的有效性
        String redisKey = request.getRemoteAddr() + "Token[" + DeviceUtil.generateDeviceHash(request) + "]";
        log.debug("redisKey: {}", redisKey);
        String redisToken = redisTemplate.opsForValue().get(redisKey);
        // 如果redis中没有token或者redis中的token和Authorization头中的token不一致，则验证失败
        if (redisToken == null || !redisToken.equals(cookieToken)) {
            log.debug("redisToken: {}", redisToken);
            log.error("redis中没有token或者redis中的token和Authorization头中的token不一致，验证失败");
            response.sendError(401, "验证失败");
            return;
        }
        // 解析Token，验证Token的有效性
        try {
            Claims claims = JwtUtil.parseToken(cookieToken);
            // 如果无法获取到Claims，则验证失败
            if (claims == null) {
                log.debug("无法获取到Claims，验证失败");
                response.sendError(401, "验证失败");
                return;
            }
            // 验证设备指纹
            if (!DeviceUtil.generateDeviceHash(request).equals(claims.get("deviceHash"))) {
                log.debug("设备不匹配，验证失败");
                response.sendError(401, "验证失败");
                return;
            }
            // 验证用户代理
            if (!request.getHeader("User-Agent").equals(claims.get("userAgent"))) {
                log.debug("User-Agent不匹配，验证失败");
                response.sendError(401, "验证失败");
                return;
            }
            // 验证成功，设置SecurityContextHolder
            List<SimpleGrantedAuthority> authorities = Arrays.stream(User.UserType.values()).map(role ->
                    new SimpleGrantedAuthority("ROLE_" + role.name())
            ).toList();
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(
                            claims.get("deviceHash"),
                            claims.get("userAgent"),
                            authorities);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            log.debug("验证成功，设置SecurityContextHolder");
            filterChain.doFilter(request, response);
        } catch (ExpiredJwtException e) {
            log.debug("Token已过期，验证失败");
            response.sendError(401, "验证失败");
        } catch (MalformedJwtException e) {
            log.debug("Token格式错误，验证失败");
            response.sendError(401, "验证失败");
        } catch (SignatureException e) {
            log.debug("Token签名错误，验证失败");
            response.sendError(401, "验证失败");
        } catch (Exception e) {
            log.error("验证Token失败", e);
            response.sendError(401, "验证失败");
        }
    }

    @Override
    protected boolean shouldNotFilter(@NotNull HttpServletRequest request) {
        for (String white : REGISTER_WHITE_LIST) {
            if (request.getServletPath().startsWith(white)) {
                return true;
            }
        }
        return false;
    }
}

